﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Text;
using System.IO;
using System.IO.Compression;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

// User defined types.
using Properties = System.Collections.Generic.Dictionary<string, string>;
using ID_t = System.UInt32;
using System.Text.RegularExpressions;

namespace lumo.display.object2D
{
    /// <summary>
    /// Tiled map
    /// </summary>
    public sealed class Tiled
    {
        /// <summary>
        /// Tile class.
        /// </summary>
        public sealed class Tile
        {
            /// <summary>ID of the Tile.</summary>
            public uint ID = 0;
            /// <summary>Properties.</summary>
            public Properties Properties = new Properties();

            /// <summary>
            /// Load the tile.
            /// </summary>
            /// <param name="Element">XML element</param>
            /// <returns>The tile</returns>
            internal static Tile InternalLoad(XElement Element)
            {
                Tile Tile = new Tile();
                UInt32.TryParse(Element.Attribute("id").Value, out Tile.ID);

                // For every subelement
                foreach (XElement subelement in Element.Elements())
                {
                    // Properties
                    if (subelement.Name == "properties")
                    {
                        ReadProperties(subelement, Tile.Properties);
                    }
                }
                return Tile;
            }
        }

        /// <summary>
        /// Tileset class.
        /// </summary>
        public sealed class Tileset
        {
            /// <summary>Parent tiled class.</summary>
            public readonly Tiled Parent;
            /// <summary>The first Global ID of this Tileset</summary>
            public UInt32 FirstGID = 0;
            /// <summary>The Last Global ID of this Tileset</summary>
            public UInt32 LastGID = 0;
            /// <summary>The TIle WIdth</summary>
            public UInt32 TileWidth = 0;
            /// <summary>The TIle Height</summary>
            public UInt32 TileHeight = 0;
            /// <summary>The Number of X Tiles</summary>
            public UInt32 XTiles = 0;
            /// <summary>The Number of Y Tiles</summary>
            public UInt32 YTiles = 0;
            /// <summary>The properties</summary>
            public Properties Properties = new Properties();
            /// <summary>List of tiles</summary>
            public List<Tile> TileList = new List<Tile>();
            /// <summary>Texture</summary>
            public string Image = "";
            /// <summary>Image Width</summary>
            public UInt32 ImageWidth = 0;
            /// <summary>Image Height</summary>
            public UInt32 ImageHeight = 0;

            /// <summary>
            /// Constructor.
            /// </summary>
            /// <param name="Parent">Parent tiled map.</param>
            public Tileset(Tiled Parent)
            {
                this.Parent = Parent;
            }


            /// <summary>
            /// Check if global id is in this Tileset
            /// </summary>
            /// <param name="GID">GID.</param>
            public bool HasGID(UInt32 GID)
            {
                return ((GID >= FirstGID) && (GID < LastGID));
            }

            /// <summary>
            /// Check if global id is in this Tileset
            /// </summary>
            /// <param name="GID">GID.</param>
            /// <param name="SrcRect">The rect</param>
            public bool GetRectangle(UInt32 GID, ref Rectangle SrcRect)
            {
                if (HasGID(GID))
                {
                    UInt32 LocalID = GID - FirstGID;

                    SrcRect.X = (int)((LocalID % XTiles) * TileWidth);
                    SrcRect.Y = (int)((LocalID / XTiles) * TileHeight);
                    SrcRect.Width  = (int)TileWidth;
                    SrcRect.Height = (int)TileHeight;
                    return true;
                }
                return false;
            }

            /// <summary>
            /// Load the data
            /// </summary>
            /// <param name="Parent">Parent tiled map</param>
            /// <param name="Element">The XML element</param>
            /// <returns>The tileset</returns>
            internal static Tileset InternalLoad(Tiled Parent, XElement Element)
            {
                Tileset Tileset = new Tileset(Parent);
                UInt32.TryParse(Element.Attribute("firstgid").Value, out Tileset.FirstGID);
                UInt32.TryParse(Element.Attribute("tilewidth").Value, out Tileset.TileWidth);
                UInt32.TryParse(Element.Attribute("tileheight").Value, out Tileset.TileHeight);
                Tileset.LastGID = Tileset.FirstGID + (Tileset.TileWidth * Tileset.TileHeight);

                // For every subelement
                foreach (XElement subelement in Element.Elements())
                {
                    // Properties
                    if (subelement.Name == "properties")
                    {
                        ReadProperties(subelement, Tileset.Properties);
                    }
                    // Image
                    else if (subelement.Name == "image")
                    {
                        Tileset.Image = subelement.Attribute("source").Value;
                        UInt32.TryParse(subelement.Attribute("width").Value, out Tileset.ImageWidth);
                        UInt32.TryParse(subelement.Attribute("height").Value, out Tileset.ImageHeight);
                        Tileset.XTiles = Tileset.ImageWidth / Tileset.TileWidth;
                        Tileset.YTiles = Tileset.ImageHeight / Tileset.TileHeight;
                    }
                    // Image
                    else if (subelement.Name == "tile")
                    {
                        Tileset.TileList.Add(Tile.InternalLoad(subelement));
                    }
                }
                return Tileset;
            }
        }

        public class Layer
        {
            /// <summary>Parent Tiled Objcect</summary>
            public Tiled Parent = null;
            /// <summary>Width of the layer</summary>
            public uint Width = 0;
            /// <summary>Height of the layer</summary>
            public uint Height = 0;
            /// <summary>Properties of the layer.</summary>
            public Properties Properties = new Properties();

            public Layer(Tiled Parent)
            {
                this.Parent = Parent;
            }
        }

        /// <summary>
        /// Layer class
        /// </summary>
        public sealed class TileLayer : Layer
        {
            /// <summary>Data of the layer</summary>
            public ID_t[] Data = null;

            /// <summary>
            /// Constructor.
            /// </summary>
            /// <param name="Parent">The parent tiled map</param>
            public TileLayer(Tiled Parent) : base(Parent)
            {
            }

            /// <summary>
            /// Set the data using Base64 encoded string.
            /// </summary>
            /// <param name="String">The string data</param>
            internal void SetDataBase64(string String)
            {
                // Decode de string.
                byte[] ByteData = System.Convert.FromBase64String(String);

                // Convert to ID
                Data = new ID_t[ByteData.Length / sizeof(ID_t)];
                Buffer.BlockCopy(ByteData, 0, Data, 0, ByteData.Length);
            }

            /// <summary>
            /// Read the current layer
            /// </summary>
            /// <param name="Parent">The parent tiled object</param>
            /// <param name="Element">The XML element</param>
            /// <returns></returns>
            internal static TileLayer InternalLoad(Tiled Parent, XElement Element)
            {
                TileLayer Layer = new TileLayer(Parent);
                UInt32.TryParse(Element.Attribute("width").Value, out Layer.Width);
                UInt32.TryParse(Element.Attribute("height").Value, out Layer.Height);

                // For every subelement
                foreach (XElement subelement in Element.Elements())
                {
                    // Properties
                    if (subelement.Name == "properties")
                    {
                        ReadProperties(subelement, Layer.Properties);
                    }
                    // Data
                    else if (subelement.Name == "data")
                    {
                        if (subelement.Attribute("encoding").Value != "base64")
                            throw new Exception("Invalid encoding in TMX file. Did you use base64?");
                        XAttribute Compression = subelement.Attribute("compression");
                        if (Compression != null)
                            throw new Exception("Invalid compression in TMX file. No compression is allowed.");
                        Layer.SetDataBase64(subelement.Value);
                    }
                }
                return Layer;
            }
        }

        /// <summary>
        /// An object
        /// </summary>
        public sealed class Object
        {
            /// <summary>
            /// Enumeration for object types.
            /// </summary>
            public enum GeometryTypes
            {
                /// <summary>Point type object.</summary>
                Point,

                /// <summary>Polygon type object.</summary>
                Polygon,

                /// <summary>Line type object.</summary>
                Line
            }

            /// <summary>Name of object.</summary>
            public string Name;
            /// <summary>Type of object.</summary>
            public string Type;
            /// <summary>GeometryType of object.</summary>
            public GeometryTypes GeometryType = GeometryTypes.Point;
            /// <summary>X Position of the object.</summary>
            public int X = 0;
            /// <summary>Y position of the object.</summary>
            public int Y = 0;
            /// <summary>Width of the object.</summary>
            public int Width = 0;
            /// <summary>Height of the object.</summary>
            public int Height = 0;
            /// <summary>Points for object.</summary>
            public List<Vector2> Points = new List<Vector2>();
            /// <summary>Properties of the layer.</summary>
            public Properties Properties = new Properties();

            /// <summary>
            /// Get Absolute points for this object.
            /// </summary>
            /// <param name="RemoveLastIfSame"></param>
            /// <returns></returns>
            public List<Vector2> GetAbsolutePoints(bool RemoveLastIfSame = false)
            {
                List<Vector2> Points = new List<Vector2>();
                Vector2 Origin = new Vector2(X, Y);
                foreach (var Point in this.Points)
                    Points.Add(Origin + Point);
                if (RemoveLastIfSame && ((Points[0] - Points[Points.Count - 1]).Length() <= 0.5))
                    Points.RemoveAt(Points.Count - 1);
                return Points;
            }
        }

        /// <summary>
        /// Group of objects
        /// </summary>
        public sealed class ObjectGroup : Layer
        {
            /// <summary>Data of the layer</summary>
            public List<Object> ObjectList = new List<Object>();

            /// <summary>
            /// Constructor.
            /// </summary>
            /// <param name="Parent">The parent tiled map</param>
            public ObjectGroup(Tiled Parent) : base(Parent)
            {
            }

            /// <summary>
            /// Objects attribute.
            /// </summary>
            public IEnumerable<Object> Objects { get { return ObjectList; } }

            /// <summary>
            /// Read the current object group.
            /// </summary>
            /// <param name="Parent">The parent tiled object</param>
            /// <param name="Element">The XML element</param>
            /// <returns>The object group</returns>
            internal static ObjectGroup InternalLoad(Tiled Parent, XElement Element)
            {
                ObjectGroup ObjectGroup = new ObjectGroup(Parent);
                UInt32.TryParse(Element.Attribute("width").Value, out ObjectGroup.Width);
                UInt32.TryParse(Element.Attribute("height").Value, out ObjectGroup.Height);


                // For every subelement
                foreach (XElement subelement in Element.Elements())
                {
                    // Properties
                    if (subelement.Name == "properties")
                    {
                        ReadProperties(subelement, ObjectGroup.Properties);
                    }
                    // Data
                    else if (subelement.Name == "object")
                    {
                        Object Obj = new Object();
                        Int32.TryParse(subelement.Attribute("x").Value, out Obj.X);
                        Int32.TryParse(subelement.Attribute("y").Value, out Obj.Y);

                        XAttribute Name = subelement.Attribute("name");
                        XAttribute Type = subelement.Attribute("type");
                        XAttribute Width = subelement.Attribute("width");
                        XAttribute Height = subelement.Attribute("height");
                        if (Name != null)
                            Obj.Name = Name.Value;
                        if (Type != null)
                            Obj.Type = Type.Value;
                        if (Width != null)
                            Int32.TryParse(Width.Value, out Obj.Width);
                        if (Height != null)
                            Int32.TryParse(Height.Value, out Obj.Height);

                        // Check sub sub elements.
                        foreach (XElement subsubelement in subelement.Elements())
                        {
                            // Properties
                            if (subsubelement.Name == "properties")
                            {
                                ReadProperties(subsubelement, Obj.Properties);
                            }
                            // Properties
                            else if (subsubelement.Name == "polygon")
                            {
                                XAttribute points = subsubelement.Attribute("points");
                                if (points != null)
                                {
                                    foreach (string word in points.Value.Split(' '))
                                    {
                                        string[] values = word.Split(',');
                                        float tx, ty;
                                        float.TryParse(values[0], out tx);
                                        float.TryParse(values[1], out ty);
                                        Obj.Points.Add(new Vector2(tx, ty));
                                    }
                                    Obj.GeometryType = Object.GeometryTypes.Polygon;
                                }
                            }
                            // Properties
                            else if (subsubelement.Name == "polyline")
                            {
                                XAttribute points = subsubelement.Attribute("points");
                                if (points != null)
                                {
                                    foreach (string word in points.Value.Split(' '))
                                    {
                                        string[] values = word.Split(',');
                                        float tx, ty;
                                        float.TryParse(values[0], out tx);
                                        float.TryParse(values[1], out ty);
                                        Obj.Points.Add(new Vector2(tx, ty));
                                    }
                                    Obj.GeometryType = Object.GeometryTypes.Line;
                                }
                            }
                        }

                        // Add the object.
                        ObjectGroup.ObjectList.Add(Obj);
                    }
                }
                return ObjectGroup;
            }
        }

        /// <summary>Width of the map.</summary>
        public uint Width = 0;
        /// <summary>Height of the map.</summary>
        public uint Height = 0;
        /// <summary>TileWidth of the map.</summary>
        public uint TileWidth = 0;
        /// <summary>TileHeight of the map.</summary>
        public uint TileHeight = 0;
        /// <summary>Map properties.</summary>
        public Properties Properties = new Properties();
        /// <summary>List of tilesets.</summary>
        public List<Tileset> TilesetList = new List<Tileset>();
        /// <summary>List of layers.</summary>
        public List<Layer> LayerList = new List<Layer>();

        /// <summary>
        /// Constructor.
        /// </summary>
        public Tiled()
        {
            
        }

        /// <summary>
        /// Tilesets attribute.
        /// </summary>
        public IEnumerable<Tileset> Tilesets { get { return TilesetList; } }

        /// <summary>
        /// Layers attribute.
        /// </summary>
        public IEnumerable<Layer> Layers { get { return LayerList; } }

        /// <summary>
        /// Layers attribute.
        /// </summary>
        public IEnumerable<TileLayer> TileLayers { get { return LayerList.Where(t => t is TileLayer).Cast<TileLayer>(); } }

        /// <summary>
        /// ObjectGroups attribute.
        /// </summary>
        public IEnumerable<ObjectGroup> ObjectGroups { get { return LayerList.Where(t => t is ObjectGroup).Cast<ObjectGroup>(); } }

        /// <summary>
        /// Objects attribute.
        /// </summary>
        public IEnumerable<Object> Objects
        { 
            get 
            {
                foreach (ObjectGroup group in ObjectGroups)
                {
                    foreach (Object obj in group.ObjectList)
                    {
                        yield return obj;
                    }
                }
            } 
        }

        /// <summary>
        /// Load the Map
        /// </summary>
        internal static Tiled InternalLoad(XDocument Document, ContentManager Content)
        {
            Tiled Tiled = new Tiled();
            XElement map = Document.Element("map");

            UInt32.TryParse(map.Attribute("width").Value, out Tiled.Width);
            UInt32.TryParse(map.Attribute("height").Value, out Tiled.Height);
            UInt32.TryParse(map.Attribute("tilewidth").Value, out Tiled.TileWidth);
            UInt32.TryParse(map.Attribute("tileheight").Value, out Tiled.TileHeight);

            foreach (XElement el in map.Elements())
            {
                // Properties
                if (el.Name == "properties")
                {
                    ReadProperties(el, Tiled.Properties);
                }
                // Tilesets
                else if (el.Name == "tileset")
                {
                    Tiled.TilesetList.Add(Tileset.InternalLoad(Tiled, el));
                }
                // Tilesets
                else if (el.Name == "layer")
                {
                    Tiled.LayerList.Add(TileLayer.InternalLoad(Tiled, el));
                }
                // Tilesets
                else if (el.Name == "objectgroup")
                {
                    Tiled.LayerList.Add(ObjectGroup.InternalLoad(Tiled, el));
                }
            }
            return Tiled;
        }

        /// <summary>
        /// Load the Map
        /// </summary>
        public static Tiled Load(Stream Stream, ContentManager Content)
        {
            return InternalLoad(XDocument.Load(Stream), Content);
        }

        /// <summary>
        /// Load the Map
        /// </summary>
        public static Tiled Load(TextReader Reader, ContentManager Content)
        {
            return InternalLoad(XDocument.Load(Reader), Content);
        }

        /// <summary>
        /// Read the properties.
        /// </summary>
        internal static void ReadProperties(XElement element, Properties Properties)
        {
            foreach (XElement property in element.Elements())
            {
                if (property.Name == "property")
                    Properties.Add(property.Attribute("name").Value, property.Attribute("value").Value);
            }
        }
    }
}
 